﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Xml.Serialization;
using CSW.Framework.Common.Binding.Events;
using CSW.Framework.Common.Binding.Interfaces;
using CSW.Framework.Common.Binding.ValueTypes;

namespace CSW.Framework.Common.Binding
{
    /// <summary>
    /// BindableObject.
    /// </summary>
    public abstract class BindableObject : MarshalByRefObject, IBindableObject
    {
        private Dictionary<string, CheckConstraintDelegate> m_CheckConstraints;
        private Dictionary<string, List<string>> m_Dependencies;
        private List<string> m_CapturedErrors;
        private Dictionary<string, object> m_BaselineValues;
        private List<IChildListProperty> m_DetailLists;
        //private Dictionary<IBindingList, IList> m_DetailListsInitial;
        private Dictionary<string, ComplexProperty> m_ComplexPropertyDictionary;
        private ComplexPropertyContainer m_Properties;

        private BindableObjectState m_BindableObjectState = BindableObjectState.Pristine;
        private bool m_BaselineStateHasChanged;
        private bool m_IsUpdatingObjectState;

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Occurs when a property in the object has been set to an incorrect state.
        /// </summary>
        public event InvalidDataEventHandler InvalidData;

        /// <summary>
        /// Occurs when a ComplexProperty's ReadOnly value changes.
        /// </summary>
        public event ReadOnlyChangedEventHandler ReadOnlyChanged;

        /// <summary>
        /// Occurs when the ObjectState property changes.
        /// </summary>
        public event EventHandler ObjectStateChanged;

        /*/// <summary>
        /// Occurs when the object should be refreshed from its data source.
        /// </summary>
        public event EventHandler RefreshRequested;*/

        /// <summary>
        /// Occurs before the object is refreshed.
        /// </summary>
        public event EventHandler BeforeRefresh;

        /// <summary>
        /// Occurs after the object is refreshed;
        /// </summary>
        public event EventHandler AfterRefresh;

        /// <summary>
        /// Bindable Object Property.
        /// </summary>
        /// <typeparam name="T">The type of the property's value.</typeparam>
        public class Property<T> : BaseProperty<T>
        {
            private T m_Value;
            private readonly BindableObject m_BindableObject;
            //private bool m_IsLoaded;

            /// <summary>
            /// Initializes a new instance of the <see cref="Property&lt;T&gt;"/> class.
            /// </summary>
            /// <param name="bindableObject">The business object.</param>
            /// <param name="propertyName">Name of the property.</param>
            public Property(BindableObject bindableObject, string propertyName)
                : this(bindableObject, propertyName, default(T))
            {
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="Property&lt;T&gt;"/> class.
            /// </summary>
            /// <param name="bindableObject">The business object.</param>
            /// <param name="propertyName">Name of the property.</param>
            /// <param name="initialValue">The initial value.</param>
            public Property(BindableObject bindableObject, string propertyName, T initialValue)
                : this(bindableObject, propertyName, initialValue, false)
            {
            }

            /// <summary>
            /// Initializes a new instance of the <see cref="Property&lt;T&gt;"/> class.
            /// </summary>
            /// <param name="bindableObject">The business object.</param>
            /// <param name="propertyName">Name of the property.</param>
            /// <param name="initialValue">The initial value.</param>
            /// <param name="isChild">If set to <c>true</c>, indicates this property is a child and not directly related to the entity.</param>
            public Property(BindableObject bindableObject, string propertyName, T initialValue, bool isChild)
                : base(bindableObject, propertyName, isChild)
            {
                Guard.ArgumentNotNull(bindableObject, "bindableObject");
                Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");

                m_BindableObject = bindableObject;
                m_Value = initialValue;
                //m_IsLoaded = true;
            }

            /// <summary>
            /// The <see cref="BindableObject"/> which owns this property.
            /// </summary>
            public BindableObject BindableObject
            {
                get { return m_BindableObject; }
            }

            /// <summary>
            /// Gets or sets the value.
            /// </summary>
            /// <value>The value.</value>
            public override T Value
            {
                get
                {
                    /*if (!m_IsLoaded)
                        m_BindableObject.SendRefreshRequested();*/

                    return m_Value;
                }
                set
                {
                    //m_IsLoaded = true;

                    T oldValue = m_Value;

                    if (typeof(T) == typeof(string))
                    {
                        string newValue = value as string;
                        if (newValue != null)
                        {
                            newValue = newValue.Trim();

                            // Enforce MaxLength
                            if (MaxLength.HasValue)
                            {
                                if (newValue.Length > MaxLength.Value)
                                    newValue = newValue.Substring(0, MaxLength.Value);
                            }
                        }

                        object oldValueObj = m_Value;
                        string strOldValue = (string)oldValueObj;

                        // If the values are different and they are not both null or empty string
                        if (strOldValue != newValue && !(string.IsNullOrEmpty(newValue) && string.IsNullOrEmpty(strOldValue)))
                        {
                            CheckEdittable();

                            object newValueObj = newValue;
                            m_Value = (T)newValueObj;
                        }
                    }
                    else
                    {
                        // ReSharper disable CompareNonConstrainedGenericWithNull
                        if ((m_Value != null && !m_Value.Equals(value)) || (m_Value == null && value != null))
                        {
                            CheckEdittable();

                            m_Value = value;
                        }
                        // ReSharper restore CompareNonConstrainedGenericWithNull
                    }

                    SendPropertyChanged(oldValue, m_Value);

                    m_BindableObject.SendPropertyChanged(PropertyName);
                }
            }
        }

        /// <summary>
        /// Bindable Object ChildListProperty.
        /// </summary>
        /// <typeparam name="TList">The type of the property's value.</typeparam>
        /// <typeparam name="TItem">The type of the property's value.</typeparam>
        public class ChildListProperty<TList, TItem> : Property<TList>, IChildListProperty
            where TList : BindingList<TItem>
            where TItem : IBusinessObject
        {
            private readonly Func<TList> m_RefreshMethod;
            private readonly FuncVoid<IList<TItem>> m_BulkDeleteMethod;

            private readonly List<TItem> m_DatabaseList;
            private readonly List<TItem> m_BaselineList;
            private readonly SetParentDelegate m_SetParent;

            private bool m_DetailListChanged;

            private bool m_IsInitialized;
            private readonly object m_IsInitializedLocker = new object();

            /// <summary>
            /// SetParentDelegate
            /// </summary>
            public delegate void SetParentDelegate(TItem child);

            /// <summary>
            /// Initializes a new instance of the <see cref="Property&lt;T&gt;"/> class.
            /// </summary>
            /// <param name="bindableObject">The business object.</param>
            /// <param name="propertyName">Name of the property.</param>
            /// <param name="setParent">The set parent method.</param>
            /// <param name="refreshMethod">The refresh method.</param>
            /// <param name="bulkDeleteMethod">The bulk delete method (optional).</param>
            public ChildListProperty(BindableObject bindableObject, string propertyName, SetParentDelegate setParent, Func<TList> refreshMethod, FuncVoid<IList<TItem>> bulkDeleteMethod)
                : base(bindableObject, propertyName, null, true)
            {
                Guard.ArgumentNotNull(setParent, "setParent");
                Guard.ArgumentNotNull(refreshMethod, "refreshMethod");

                m_SetParent = setParent;
                m_RefreshMethod = refreshMethod;
                m_BulkDeleteMethod = bulkDeleteMethod;
                m_DatabaseList = new List<TItem>();
                m_BaselineList = new List<TItem>();
            }

            /// <summary>
            /// Returns <c>true</c> if the detail list has changes.
            /// </summary>
            /// <returns><c>true</c> if the detail list has changes; otherwise, <c>false</c>.</returns>
            public bool GetDetailListHasChanges()
            {
                if (m_DetailListChanged)
                    return true;

                CheckInitialized();

                foreach (TItem item in Value)
                {
                    if (item.ObjectState != BindableObjectState.Pristine)
                        return true;
                }

                return false;
            }

            /*public void SetInitialState()
            {
                m_DetailListChanged = false;
            }*/

            /// <summary>
            /// Tries to save the record's current state as its baseline state.
            /// </summary>
            /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
            public bool TrySaveBaselineState(out string errorMessage)
            {
                CheckInitialized();

                foreach (TItem item in Value)
                {
                    if (!item.TrySaveBaselineState(out errorMessage))
                        return false;
                }

                errorMessage = null;
                return true;
            }

            /// <summary>
            /// Reverts the state of the list to baseline.
            /// </summary>
            public void RevertToBaselineState()
            {
                CheckInitialized();

                // TODO: Another state may be needed.. look into
                // It'll handle the difference between reverting to baseline and reverting to parent-saved
                // Must think about multiple levels of save/cancel.

                if (m_DetailListChanged)
                {
                    Value.Clear();
                    foreach (TItem item in m_BaselineList)
                    {
                        Value.Add(item);
                    }
                    // don't revert this - listchanged applies to Database, not Baseline
                    //m_DetailListChanged = false;
                }

                foreach (TItem item in Value)
                {
                    item.RevertToBaselineState();
                }

                BindableObject.UpdateObjectState();
            }

            private bool CheckInitialized()
            {
                bool refreshed = false;

                if (!m_IsInitialized)
                {
                    lock (m_IsInitializedLocker)
                    {
                        if (!m_IsInitialized)
                        {
                            base.Value = m_RefreshMethod.Invoke();
                            m_IsInitialized = true;

                            m_DatabaseList.Clear();
                            foreach (TItem item in Value)
                                m_DatabaseList.Add(item);

                            m_BaselineList.Clear();
                            foreach (TItem item in Value)
                                m_BaselineList.Add(item);

                            refreshed = true;

                            Value.ListChanged += ListChangedHandler;
                        }
                    }
                }

                return refreshed;
            }

            /// <summary>
            /// Gets a value indicating whether this instance is initialized.
            /// </summary>
            /// <value><c>true</c> if this instance is initialized; otherwise, <c>false</c>.</value>
            public bool IsInitialized
            {
                get { return m_IsInitialized; }
            }

            private void ListChangedHandler(object sender, ListChangedEventArgs e)
            {
                CheckInitialized();

                BindableObject.SendPropertyChanged(PropertyName);

                if (e.ListChangedType == ListChangedType.ItemAdded)
                {
                    TItem item = Value[e.NewIndex];
                    m_SetParent(item);
                    m_DetailListChanged = true;
                    BindableObject.ObjectState = BindableObjectState.Changed;
                }
                else if (e.ListChangedType == ListChangedType.ItemDeleted)
                {
                    m_DetailListChanged = true;
                    BindableObject.ObjectState = BindableObjectState.Changed;
                }
                else if (!m_DetailListChanged)
                {
                    bool hasChanges = GetDetailListHasChanges();
                    if (hasChanges)
                        BindableObject.ObjectState = BindableObjectState.Changed;
                    else
                        BindableObject.UpdateObjectState();
                }
            }

            /// <summary>
            /// Refreshes this instance.
            /// </summary>
            public void Refresh()
            {
                if (CheckInitialized())
                    return;

                TList newValue = m_RefreshMethod.Invoke();
                Value = newValue;
            }

            /// <summary>
            /// Gets the value.
            /// </summary>
            /// <value>The value.</value>
            public override TList Value
            {
                get
                {
                    CheckInitialized();

                    return base.Value;
                }
                set
                {
                    Guard.ArgumentNotNull(value, "value");

                    if (Value != null)
                        Value.ListChanged -= ListChangedHandler;

                    base.Value = value;
                    Value.ListChanged -= ListChangedHandler;

                    m_DetailListChanged = false;

                    m_DatabaseList.Clear();
                    foreach (TItem item in Value)
                        m_DatabaseList.Add(item);

                    m_BaselineList.Clear();
                    foreach (TItem item in Value)
                        m_BaselineList.Add(item);

                    Value.ListChanged += ListChangedHandler;

                    BindableObject.UpdateObjectState();
                }
            }

            IBindingList IChildListProperty.Value
            {
                get { return Value; }
            }

            /// <summary>
            /// Tries to save the record to the database. It is this method's responsibility to <see cref="BindableObject.CheckAllRules(out string)"/> and set <see cref="BusinessObject.IsEditting"/> to false.
            /// </summary>
            /// <param name="errorMessage">The error message.</param>
            /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
            public bool TrySave(out string errorMessage)
            {
                CheckInitialized();

                List<TItem> deleteList = new List<TItem>();

                TList value = Value;
                foreach (TItem initialItem in m_DatabaseList)
                {
                    if (!value.Contains(initialItem))
                        deleteList.Add(initialItem);
                }

                if (deleteList.Count > 0)
                    BulkDelete(deleteList);

                // Inserts/Updates
                foreach (TItem item in value)
                {
                    if (item.IsNew || item.ObjectState != BindableObjectState.Pristine)
                    {
                        if (!item.TrySave(out errorMessage))
                        {
                            return false;
                        }
                    }
                }

                errorMessage = null;
                return true;
            }

            private void BulkDelete(IList<TItem> deleteList)
            {
                Guard.ArgumentNotNull(deleteList, "deleteList");

                if (m_BulkDeleteMethod != null)
                {
                    m_BulkDeleteMethod(deleteList);
                }
                else
                {
                    foreach (TItem item in deleteList)
                        item.Delete();
                }
            }
        }

        /// <summary>
        /// BindableObject BaseProperty.
        /// </summary>
        public abstract class BaseProperty<T> : ComplexProperty<T>
        {
            private readonly BindableObject m_BindableObject;
            private readonly string m_PropertyName;
            private readonly bool m_IsChild;

            private bool m_IsReadOnly;
            private int? m_MaxLength;
            private bool m_Override;
            private bool m_IsRequired;
            //private bool m_IsLoaded;

            /// <summary>
            /// PropertyChangedEventHandler.
            /// </summary>
            public delegate void PropertyChangedEventHandler(ComplexProperty<T> sender, PropertyChangedEventArgs e);

            /// <summary>
            /// Occurs when the property value changes.
            /// </summary>
            public event PropertyChangedEventHandler PropertyChanged;

            /// <summary>
            /// Initializes a new instance of the <see cref="BaseProperty&lt;T&gt;"/> class.
            /// </summary>
            /// <param name="bindableObject">The business object.</param>
            /// <param name="propertyName">Name of the property.</param>
            /// <param name="isChild">If set to <c>true</c>, indicates this property is a child and not directly related to the entity.</param>
            protected BaseProperty(BindableObject bindableObject, string propertyName, bool isChild)
            {
                Guard.ArgumentNotNull(bindableObject, "bindableObject");
                Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");

                m_BindableObject = bindableObject;
                m_PropertyName = propertyName;
                //m_IsLoaded = true;
                m_IsChild = isChild;

                m_BindableObject.AddComplexProperty(this);
            }

            /// <summary>
            /// Sends the property changed event.
            /// </summary>
            /// <param name="oldValue">The old value.</param>
            /// <param name="newValue">The new value.</param>
            protected void SendPropertyChanged(T oldValue, T newValue)
            {
                PropertyChangedEventHandler eventHandler = PropertyChanged;
                if (eventHandler != null)
                    eventHandler(this, new PropertyChangedEventArgs(oldValue, newValue));
                // TODO: Should probably be calling BindableObject.SendPropertyChanged, needs investigation
            }

            /// <summary>
            /// Gets or sets a value indicating whether this instance is a child.
            /// </summary>
            /// <value><c>true</c> if this instance is a child; otherwise, <c>false</c>.</value>
            public override bool IsChild
            {
                get { return m_IsChild; }
            }

            internal BindableObject Owner
            {
                get { return m_BindableObject; }
            }

            /*/// <summary>
            /// Gets or sets a value indicating whether this instance is loaded.
            /// If set to <c>false</c>, the property getter will request an object refresh on get.
            /// Setting the property's value will set IsLoaded to true and further gets on the property will not trigger a refresh.
            /// </summary>
            /// <value><c>true</c> if this instance is loaded; otherwise, <c>false</c>.</value>
            public override bool IsLoaded
            {
                get { return m_IsLoaded; }
                set
                {
                    CheckCallingType();
                    m_IsLoaded = value;
                }
            }*/

            /// <summary>
            /// Gets the maximum string length.
            /// </summary>
            /// <value>The maximum string length.</value>
            public override int? MaxLength
            {
                get { return m_MaxLength; }
                set
                {
                    CheckCallingType();

                    if (value == null)
                        throw new ArgumentOutOfRangeException("value", value, "maxLength must not be null.");

                    if (m_MaxLength != null)
                        throw new ArgumentException("MaxLength already set.", "value");

                    if (value <= 0)
                        throw new ArgumentOutOfRangeException("value", value, "maxLength must be greater than 0.");

                    if (typeof(T) != typeof(String))
                        throw new ArgumentException(String.Format("maxLength only supported for ComplexProperty<String>.  Property = '{0}'.", m_PropertyName), "value");

                    m_MaxLength = value;
                }
            }

            /// <summary>
            /// Gets the property name.
            /// </summary>
            /// <value>The property name.</value>
            public override string PropertyName
            {
                get { return m_PropertyName; }
            }

            /// <summary>
            /// Checks if the instance is edittable.
            /// </summary>
            /// <exception cref="InvalidOperationException">throws <see cref="InvalidOperationException"/> if the value cannot be changed.</exception>
            public void CheckEdittable()
            {
                if (!m_Override)
                {
                    m_BindableObject.CheckEdittable();

                    if (m_IsReadOnly && !m_BindableObject.IsLoading)
                        throw new InvalidOperationException(string.Format("Property '{0}' is read only.", m_PropertyName));
                }
            }

            /// <summary>
            /// Overrides a value, even if it is read only.  Intended to allow subclasses to override the value
            /// of a calculated field without needing to reimplement the Value set/get, and maintaining read only
            /// access to class users.
            /// </summary>
            /// <param name="value">The value.</param>
            public void OverrideReadOnlyValue(T value)
            {
                CheckCallingType();

                m_Override = true;
                try
                {
                    Value = value;
                }
                finally
                {
                    m_Override = false;
                }
            }

            /// <summary>
            /// Gets a value indicating whether the property is read only.
            /// </summary>
            /// <value><c>true</c> if the property is read only; otherwise, <c>false</c>.</value>
            public override bool IsReadOnly
            {
                get { return m_IsReadOnly; }
                set
                {
                    CheckCallingType();

                    if (m_IsReadOnly != value)
                    {
                        m_IsReadOnly = value;
                        ReadOnlyChangedEventHandler readOnlyChanged = m_BindableObject.ReadOnlyChanged;
                        if (readOnlyChanged != null)
                            readOnlyChanged(m_BindableObject, new ReadOnlyChangedEventArgs(m_PropertyName, value));
                    }
                }
            }

            /// <summary>
            /// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
            /// </summary>
            /// <returns>
            /// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
            /// </returns>
            public override string ToString()
            {
                // ReSharper disable CompareNonConstrainedGenericWithNull
                return Value == null ? null : Value.ToString();
                // ReSharper restore CompareNonConstrainedGenericWithNull
            }

            /// <summary>
            /// Gets or sets a value indicating whether this field is required.
            /// </summary>
            /// <value><c>true</c> if this field is required; otherwise, <c>false</c>.</value>
            public bool IsRequired
            {
                get { return m_IsRequired; }
                set
                {
                    CheckCallingType();

                    m_IsRequired = value;
                    m_BindableObject.CheckRules(m_PropertyName);
                }
            }

            private void CheckCallingType()
            {
                Type callingType = (new StackTrace()).GetFrame(2).GetMethod().DeclaringType;
                Type originalCallingType = callingType;
                while (callingType != typeof(object))
                {
                    if (callingType == typeof(BindableObject) || callingType == typeof(ComplexProperty))
                        return;

                    callingType = callingType.BaseType;
                }

                throw new Exception(String.Format("'{0}' is not the owner of '{1}.{2}'", originalCallingType, m_BindableObject.GetType(), m_PropertyName));
            }

            /// <summary>
            /// PropertyChangedEventArgs.
            /// </summary>
            public class PropertyChangedEventArgs : EventArgs
            {
                private readonly T m_OldValue;
                private readonly T m_NewValue;

                /// <summary>
                /// PropertyChangedEventArgs constructor.
                /// </summary>
                /// <param name="oldValue">The old value.</param>
                /// <param name="newValue">The new value.</param>
                public PropertyChangedEventArgs(T oldValue, T newValue)
                {
                    m_OldValue = oldValue;
                    m_NewValue = newValue;
                }

                /// <summary>
                /// Gets the old value.
                /// </summary>
                public T OldValue { get { return m_OldValue; } }

                /// <summary>
                /// Gets the new value.
                /// </summary>
                public T NewValue { get { return m_NewValue; } }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is initialized.
        /// </summary>
        /// <value><c>true</c> if this instance is initialized; otherwise, <c>false</c>.</value>
        protected bool IsInitialized { get; private set; }

        /// <summary>
        /// Gets the detail lists.
        /// </summary>
        /// <value>The detail lists.</value>
        [XmlIgnore]
        public IEnumerable<IChildListProperty> DetailLists
        {
            get
            {
                CheckInitialized();
                return m_DetailLists;
            }
        }

        /// <summary>
        /// Gets the object's state (Initial, Dirty).
        /// </summary>
        /// <value>The object's state (Initial, Dirty).</value>
        [XmlIgnore]
        public BindableObjectState ObjectState
        {
            get
            {
                return m_BindableObjectState;
            }

            private set
            {
                // once the baseline state has changed, the object can no longer be considered prestine
                if (m_BaselineStateHasChanged && value == BindableObjectState.Pristine)
                {
                    throw new InvalidOperationException("Internal error: Once the baseline state has changed, the object can no longer be considered pristine.");
                }

                if (m_BindableObjectState != value)
                {
                    m_BindableObjectState = value;

                    SendPropertyChanged("ObjectState");

                    EventHandler stateChanged = ObjectStateChanged;
                    if (stateChanged != null)
                        stateChanged(this, EventArgs.Empty);
                }
            }
        }

        /// <summary>
        /// Checks all business rules.
        /// </summary>
        /// <returns><c>true</c> if all rules pass; otherwise, <c>false</c>.</returns>
        public bool CheckAllRules()
        {
            CheckInitialized();

            bool allOK = true;
            foreach (KeyValuePair<string, CheckConstraintDelegate> checkRules in m_CheckConstraints)
            {
                ErrorMessage errorMessage = new ErrorMessage();
                checkRules.Value(errorMessage);

                if (errorMessage.HasErrors)
                {
                    SendInvalidData(checkRules.Key, errorMessage.ToString());
                    allOK = false;
                }
                else
                {
                    ClearInvalidData(checkRules.Key);
                }
            }

            // Check rules for child items
            foreach (IChildListProperty list in m_DetailLists)
            {
                if (!list.IsInitialized)
                    continue;

                if (list.GetDetailListHasChanges())
                {
                    foreach (IBusinessObject item in list.Value)
                    {
                        string errorMessage;
                        if (!item.CheckAllRules(out errorMessage))
                        {
                            SendInvalidData(list.PropertyName, string.Format("{1}{0}:{1}{2}{1}", list.PropertyName, Environment.NewLine, errorMessage));
                            allOK = false;
                        }
                    }
                }
            }

            return allOK;
        }

        /// <summary>
        /// Checks all business rules.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        /// <returns><c>true</c> if all rules pass; otherwise, <c>false</c>.</returns>
        public bool CheckAllRules(out string errorMessage)
        {
            CheckInitialized();

            ClearCapturedErrors();
            bool success;

            InvalidData += CaptureErrors;
            try
            {
                success = CheckAllRules();

                if (!success)
                {
                    errorMessage = string.Empty;
                    foreach (string singleErrorMessage in m_CapturedErrors)
                    {
                        if (!string.IsNullOrEmpty(errorMessage)) errorMessage += Environment.NewLine;
                        errorMessage += singleErrorMessage;
                    }
                }
                else
                {
                    errorMessage = null;
                }
            }
            finally
            {
                InvalidData -= CaptureErrors;
            }

            return success;
        }

        /// <summary>
        /// Tries to save the record's current state as its baseline state.
        /// </summary>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool TrySaveBaselineState()
        {
            CheckInitialized();

            string errorMessage;
            return TrySaveBaselineState(out errorMessage);
        }

        /// <summary>
        /// Tries to save the record's current state as its baseline state.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        /// <returns><c>true</c> if successful; otherwise, <c>false</c>.</returns>
        public bool TrySaveBaselineState(out string errorMessage)
        {
            CheckInitialized();

            if (!CheckAllRules(out errorMessage))
                return false;

            m_BaselineValues.Clear();
            foreach (ComplexProperty item in Properties)
            {
                if (item.IsChild)
                    continue;

                // having a lazy loaded property at this point is an error
                /*if (!item.IsLoaded)
                {
                    throw new Exception(string.Format("Internal error: Property '{0}' is currently in lazy loaded state.", item.PropertyName));
                }*/

                m_BaselineValues.Add(item.PropertyName, item.UnsafeValue);
            }

            foreach (IChildListProperty list in m_DetailLists)
            {
                if (!list.IsInitialized)
                    continue;

                if (!list.TrySaveBaselineState(out errorMessage))
                    return false;
            }

            m_BaselineStateHasChanged = true;
            ObjectState = BindableObjectState.NotChangedSinceLastPersist;

            return true;
        }

        /// <summary>
        /// Reverts the object to its baseline state.
        /// </summary>
        public void RevertToBaselineState()
        {
            CheckInitialized();

            // TODO: Add to initial values when lazy loaded property loaded?
            // TODO: Does accessing a lazy loaded property cause all other values to refresh?

            BeginLoading();
            try
            {
                foreach (ComplexProperty item in Properties)
                {
                    object value;
                    if (m_BaselineValues.TryGetValue(item.PropertyName, out value))
                        item.UnsafeValue = value;
                }

                foreach (IChildListProperty list in m_DetailLists)
                {
                    if (!list.IsInitialized)
                        continue;

                    list.RevertToBaselineState();
                }
            }
            finally
            {
                EndLoading();
            }
        }

        /// <summary>
        /// Gets a value indicating whether the object is loading.
        /// </summary>
        /// <value><c>true</c> if the object is loading; otherwise, <c>false</c>.</value>
        protected bool IsLoading { get; private set; }

        private int m_LoadingCount;

        /// <summary>
        /// Call when loading is started.
        /// </summary>
        protected void BeginLoading()
        {
            m_LoadingCount++;
            IsLoading = (m_LoadingCount > 0);
        }

        /// <summary>
        /// Call when loading is finished.
        /// </summary>
        protected virtual void EndLoading()
        {
            m_LoadingCount--;
            IsLoading = (m_LoadingCount > 0);

            if (m_LoadingCount < 0)
                throw new InvalidOperationException("EndLoading called without BeginLoading");

            //PopulateInitialValues();
        }

        /// <summary>
        /// Populates the initial values.
        /// </summary>
        protected void PopulateInitialValues()
        {
            CheckInitialized();

            if (m_BaselineStateHasChanged)
                throw new InvalidOperationException("PopulateInitialValues cannot be called once TrySaveInitialValues has been called.");

            ObjectState = BindableObjectState.Pristine;

            m_BaselineValues.Clear();
            foreach (ComplexProperty item in Properties)
            {
                if (item.IsChild)
                    continue;

                m_BaselineValues.Add(item.PropertyName, item.UnsafeValue);

                /*if (item.IsLoaded) // do not add lazy loaded properties
                    m_BaselineValues.Add(item.PropertyName, item.UnsafeValue);
                else
                {
                }*/
            }

            // TODO: Why not???? Why is this commented?
            /*// Populate initial values for detail lists
            foreach (IDetailListInfo info in m_DetailLists)
            {
                PopulateInitialValues(info);
            }*/
        }

        /*/// <summary>
        /// Populates the initial values of an <see cref="IDetailListInfo"/>.
        /// </summary>
        /// <param name="info">The <see cref="IDetailListInfo"/>.</param>
        protected void PopulateInitialValues(IDetailListInfo info)
        {
            CheckInitialized();

            info.SetInitialState();
            IBindingList bindingList = info.DetailList;

            IList initialList;
            if (!m_DetailListsInitial.TryGetValue(bindingList, out initialList))
                throw new Exception("Detail List not found in m_DetailListsInitial");

            initialList.Clear();
            foreach (object item in bindingList)
                initialList.Add(item);
        }*/

        /*/// <summary>
        /// Clears the detail lists.
        /// </summary>
        protected void ClearDetailLists()
        {
            CheckInitialized();

            m_DetailListsInitial.Clear();
            m_DetailLists.Clear();
        }*/

        /// <summary>
        /// Fires the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        protected void SendPropertyChanged(string propertyName)
        {
            CheckInitialized();

            PropertyChangedEventHandler propertyChanged = PropertyChanged;
            if (propertyChanged != null)
                propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Fires the <see cref="InvalidData"/> event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="message">The message.</param>
        protected void SendInvalidData(string propertyName, string message)
        {
            CheckInitialized();

            InvalidDataEventHandler invalidData = InvalidData;
            if (invalidData != null)
                invalidData(this, new InvalidDataEventArgs(propertyName, message));
        }

        /// <summary>
        /// Clears the invalid data state for a property by firing the InvalidData event with ErrorType.None.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        protected void ClearInvalidData(string propertyName)
        {
            CheckInitialized();

            InvalidDataEventHandler invalidData = InvalidData;
            if (invalidData != null)
                invalidData(this, new InvalidDataEventArgs(propertyName, ErrorType.None));
        }

        /*/// <summary>
        /// Fires the <see cref="RefreshRequested"/> event.
        /// </summary>
        protected void SendRefreshRequested()
        {
            CheckInitialized();

            EventHandler refreshRequested = RefreshRequested;
            if (refreshRequested != null)
                refreshRequested(this, EventArgs.Empty);
        }*/

        /// <summary>
        /// Sends the <see cref="BeforeRefresh"/> event.
        /// </summary>
        protected void SendBeforeRefresh()
        {
            CheckInitialized();

            EventHandler beforeRefresh = BeforeRefresh;
            if (beforeRefresh != null)
                beforeRefresh(this, EventArgs.Empty);
        }

        /// <summary>
        /// Sends the <see cref="AfterRefresh"/> event.
        /// </summary>
        protected void SendAfterRefresh()
        {
            CheckInitialized();

            EventHandler afterRefresh = AfterRefresh;
            if (afterRefresh != null)
                afterRefresh(this, EventArgs.Empty);
        }

        /// <summary>
        /// Checks the business rules for a given property.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns><c>true</c> if rule(s) pass; otherwise, <c>false</c>.</returns>
        public bool CheckRules(string propertyName)
        {
            Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");

            CheckInitialized();

            bool allOK = true;
            CheckConstraintDelegate checkRules;
            if (m_CheckConstraints.TryGetValue(propertyName, out checkRules))
            {
                ErrorMessage errorMessage = new ErrorMessage();
                checkRules(errorMessage);

                if (errorMessage.HasErrors)
                {
                    SendInvalidData(propertyName, errorMessage.ToString());
                    allOK = false;
                }
                else
                {
                    ClearInvalidData(propertyName);
                }
            }
            return allOK;
        }

        /// <summary>
        /// Checks the business rules for a given property.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="errorMessage">The error message.</param>
        /// <returns><c>true</c> if rule(s) pass; otherwise, <c>false</c>.</returns>
        public bool CheckRules(string propertyName, out string errorMessage)
        {
            CheckInitialized();

            ClearCapturedErrors();
            bool success;

            InvalidData += CaptureErrors;
            try
            {
                success = CheckRules(propertyName);

                if (!success)
                {
                    errorMessage = string.Empty;
                    foreach (string singleErrorMessage in m_CapturedErrors)
                    {
                        if (!string.IsNullOrEmpty(errorMessage)) errorMessage += Environment.NewLine;
                        errorMessage += singleErrorMessage;
                    }
                }
                else
                {
                    errorMessage = null;
                }
            }
            finally
            {
                InvalidData -= CaptureErrors;
            }

            return success;
        }

        /*/// <summary>
        /// Adds a detail list.
        /// </summary>
        /// <typeparam name="TBusinessObject">The type of the business object.</typeparam>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="detailList">The detail list.</param>
        /// <returns>The detail list info.</returns>
        protected virtual IDetailListInfo AddDetailList<TBusinessObject>(string propertyName, BindingList<TBusinessObject> detailList)
            where TBusinessObject : class, IBusinessObject
        {
            Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");
            Guard.ArgumentNotNull(detailList, "detailList");

            IDetailListInfo info = null;
            foreach (IDetailListInfo item in new List<IDetailListInfo>(m_DetailLists))
            {
                if (item.DetailList == detailList)
                {
                    Debug.Assert(item.PropertyName == propertyName);

                    info = item;
                    break;
                }
                else if (item.PropertyName == propertyName)
                {
                    // TODO: we're swapping out a detail list.. this should be
                    // TODO: done more elegantly. possible orphaning of "current" child list

                    m_DetailLists.Remove(item);
                    break;
                }
            }

            if (info == null)
            {
                info = new DetailListInfo<TBusinessObject>(this, propertyName, detailList);
                m_DetailLists.Add(info);
                //m_DetailListsInitial.Add(detailList, detailList);
                m_DetailListsInitial.Add(detailList, new List<TBusinessObject>());
            }

            PopulateInitialValues(info);

            return info;
        }*/

        /// <summary>
        /// Adds a not null rule for the specified property.  Handles String.Empty for strings.
        /// </summary>
        /// <typeparam name="T">The property type.</typeparam>
        /// <param name="complexProperty">The complex property.</param>
        /// <param name="errorMessage">The error message.</param>
        protected void AddRequiredField<T>(BaseProperty<T> complexProperty, string errorMessage)
        {
            CheckInitialized();

            AddCheckConstraint(complexProperty, CreateNotNullChecker(complexProperty, errorMessage));
        }

        /// <summary>
        /// Adds a check constraint.
        /// </summary>
        /// <param name="complexProperty">The complex property.</param>
        /// <param name="checkConstraint">The check constraint delegate.</param>
        protected void AddCheckConstraint(ComplexProperty complexProperty, CheckConstraintDelegate checkConstraint)
        {
            CheckInitialized();

            //if (((BaseProperty<T>)complexProperty).Owner != this)
            //    throw new ArgumentException(String.Format("'{0}' is not the owner of '{1}.{2}'", GetType().FullName, ((BaseProperty<T>)complexProperty).Owner.GetType(), complexProperty.PropertyName));

            //Guard.ArgumentNotNullOrEmptyString(propertyName, "propertyName");
            //Guard.ArgumentNotNull(checkConstraint, "checkConstraint");

            foreach (DependsOnAttribute attribute in checkConstraint.Method.GetCustomAttributes(typeof(DependsOnAttribute), true))
            {
                // ReSharper disable AccessToModifiedClosure
                if (!Properties.Where(p => p.PropertyName == attribute.PropertyName).Any())
                {
                    throw new InvalidOperationException(string.Format("Property '{0}' does not exist in '{1}'.", attribute.PropertyName, GetType().FullName));
                }
                // ReSharper restore AccessToModifiedClosure

                List<String> dependencyList;
                if (m_Dependencies.TryGetValue(attribute.PropertyName, out dependencyList))
                {
                    dependencyList.Add(complexProperty.PropertyName);
                }
                else
                {
                    dependencyList = new List<String>();
                    dependencyList.Add(complexProperty.PropertyName);
                    m_Dependencies.Add(attribute.PropertyName, dependencyList);
                }
            }

            CheckConstraintDelegate existingCheckConstraint;
            if (m_CheckConstraints.TryGetValue(complexProperty.PropertyName, out existingCheckConstraint))
            {
                // Remove constraint, append new delegate, add combined constraints back to dictionary
                m_CheckConstraints.Remove(complexProperty.PropertyName);
                existingCheckConstraint += checkConstraint;
                m_CheckConstraints.Add(complexProperty.PropertyName, existingCheckConstraint);
            }
            else
            {
                // Add constraint to dictionary
                m_CheckConstraints.Add(complexProperty.PropertyName, checkConstraint);
            }
        }

        /// <summary>
        /// Throws an exception if the object is not edittable.
        /// </summary>
        protected virtual void CheckEdittable()
        {
        }

        /*/// <summary>
        /// Gets a detail list's initial list of items before editting.
        /// </summary>
        /// <param name="detailList">The detail list.</param>
        /// <returns>The detail list's initial list of items before editting.</returns>
        protected IList GetInitialDetailList(IBindingList detailList)
        {
            Guard.ArgumentNotNull(detailList, "detailList");

            CheckInitialized();

            IList initialList;
            if (!m_DetailListsInitial.TryGetValue(detailList, out initialList))
                throw new Exception("Detail List not found in m_DetailListsInitial");

            return initialList;
        }*/

        /// <summary>
        /// Resets the state. Use with caution.
        /// </summary>
        protected void ResetState()
        {
            CheckInitialized();

            if (ObjectState == BindableObjectState.Pristine)
                return;

            //bool baselineStateHasChanged = m_BaselineStateHasChanged;
            m_BaselineStateHasChanged = false;
            ObjectState = BindableObjectState.Pristine;

            /*UpdateObjectState();

            if (ObjectState != BindableObjectState.Pristine && baselineStateHasChanged)
            {
                m_BaselineStateHasChanged = true;
                UpdateObjectState();
            }*/
        }

        private void BindableObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            CheckInitialized();

            if (!IsLoading)
            {
                CheckRules(e.PropertyName);

                List<string> dependencyList;
                if (m_Dependencies.TryGetValue(e.PropertyName, out dependencyList))
                {
                    foreach (string dependentPropertyName in dependencyList)
                        CheckRules(dependentPropertyName);
                }

                object initialValue;
                if (m_BaselineValues.TryGetValue(e.PropertyName, out initialValue))
                {
                    PropertyInfo propertyInfo = GetType().GetProperty(e.PropertyName);
                    if (propertyInfo == null)
                        throw new InvalidOperationException(string.Format("'{0}' does not contain a property named '{1}'.", GetType().FullName, e.PropertyName));
                    MethodInfo methodInfo = propertyInfo.GetGetMethod();
                    if (methodInfo == null)
                        throw new InvalidOperationException(string.Format("'{0}' does not contain a public get for property '{1}'.", GetType().FullName, e.PropertyName));

                    object newValue = methodInfo.Invoke(this, null);
                    if ((initialValue != null && !initialValue.Equals(newValue)) || (initialValue == null && newValue != null))
                    {
                        ObjectState = BindableObjectState.Changed;
                    }
                    else
                    {
                        UpdateObjectState();
                    }
                }
            }
        }

        private void UpdateObjectState()
        {
            CheckInitialized();

            if (!m_IsUpdatingObjectState)
            {
                m_IsUpdatingObjectState = true;
                try
                {
                    object initialValue;
                    bool isDirty = false;
                    foreach (KeyValuePair<string, object> kvp in m_BaselineValues)
                    {
                        initialValue = kvp.Value;

                        PropertyInfo propertyInfo = GetType().GetProperty(kvp.Key);
                        if (propertyInfo == null)
                            throw new Exception(string.Format("'{0}' property '{1}' does not exist.", GetType().FullName, kvp.Key));

                        MethodInfo methodInfo = propertyInfo.GetGetMethod();
                        if (methodInfo == null)
                            throw new Exception(string.Format("'{0}' property '{1}' does not have a get.", GetType().FullName, kvp.Key));

                        object newValue = methodInfo.Invoke(this, null);

                        if ((initialValue != null && !initialValue.Equals(newValue)) ||
                            (initialValue == null && newValue != null))
                        {
                            isDirty = true;
                            ObjectState = BindableObjectState.Changed;
                            break;
                        }
                    }

                    // If all the properties are the same, check the detail lists
                    if (!isDirty)
                    {
                        foreach (IChildListProperty list in m_DetailLists)
                        {
                            if (!list.IsInitialized)
                                continue;

                            if (list.GetDetailListHasChanges())
                            {
                                isDirty = true;
                                break;
                            }
                        }
                    }

                    if (!isDirty)
                    {
                        if (!m_BaselineStateHasChanged)
                            ObjectState = BindableObjectState.Pristine;
                        else
                            ObjectState = BindableObjectState.NotChangedSinceLastPersist;
                    }
                }
                finally
                {
                    m_IsUpdatingObjectState = false;
                }
            }
            else
            {
                // how'd we get here?
            }
        }

        private void CaptureErrors(object sender, InvalidDataEventArgs e)
        {
            CheckInitialized();

            if (e.ErrorType != ErrorType.None)
                m_CapturedErrors.Add(e.Message);
        }

        private void ClearCapturedErrors()
        {
            CheckInitialized();

            m_CapturedErrors.Clear();
        }

        /// <summary>
        /// Creates a delegate which validates a property is not null or empty string.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="complexProperty">The complex property.</param>
        /// <param name="errorMessage">The error message.</param>
        /// <returns>
        /// A delegate which validates a property is not null or empty string.
        /// </returns>
        private static CheckConstraintDelegate CreateNotNullChecker<T>(BaseProperty<T> complexProperty, string errorMessage)
        {
            complexProperty.IsRequired = true;

            if (typeof(T) == typeof(string))
            {
                ComplexProperty<string> stringProperty = (ComplexProperty<string>)(object)complexProperty;

                // ComplexProperty<string>
                return delegate(ErrorMessage outErrorMessage)
                {
                    if (complexProperty.IsRequired && string.IsNullOrEmpty(stringProperty.Value))
                    {
                        outErrorMessage.Append(errorMessage);
                    }
                };
            }
            else
            {
                // ComplexProperty<T> where T is not String
                return delegate(ErrorMessage outErrorMessage)
                {
                    // ReSharper disable CompareNonConstrainedGenericWithNull
                    if (complexProperty.IsRequired && complexProperty.Value == null)
                    {
                        outErrorMessage.Append(errorMessage);
                    }
                    // ReSharper restore CompareNonConstrainedGenericWithNull
                };
            }
        }

        private void AddComplexProperty(ComplexProperty complexProperty)
        {
            Guard.ArgumentNotNull(complexProperty, "complexProperty");

            CheckInitializedPrivate(); // do not cause virtual CheckInitialized to execute

            m_ComplexPropertyDictionary.Add(complexProperty.PropertyName, complexProperty);
            if (complexProperty is IChildListProperty)
                m_DetailLists.Add((IChildListProperty)complexProperty);
        }

        /// <summary>
        /// Gets the properties.
        /// </summary>
        /// <value>The properties.</value>
        public IComplexPropertyContainer Properties
        {
            get
            {
                CheckInitialized();
                return m_Properties;
            }
        }

        /// <summary>
        /// Checks the initialized.
        /// </summary>
        protected virtual void CheckInitialized()
        {
            CheckInitializedPrivate();
        }

        private void CheckInitializedPrivate()
        {
            if (!IsInitialized)
            {
                m_CheckConstraints = new Dictionary<string, CheckConstraintDelegate>();
                m_Dependencies = new Dictionary<string, List<string>>();
                m_CapturedErrors = new List<string>();
                m_BaselineValues = new Dictionary<string, object>();
                m_DetailLists = new List<IChildListProperty>();
                //m_DetailListsInitial = new Dictionary<IBindingList, IList>();

                m_ComplexPropertyDictionary = new Dictionary<string, ComplexProperty>();
                m_Properties = new ComplexPropertyContainer(this);

                PropertyChanged += BindableObject_PropertyChanged;

                IsInitialized = true;
            }
        }

        /// <summary>
        /// ComplexPropertyContainer
        /// </summary>
        private class ComplexPropertyContainer : IComplexPropertyContainer
        {
            private readonly BindableObject m_BindableObject;

            internal ComplexPropertyContainer(BindableObject bindableObject)
            {
                Guard.ArgumentNotNull(bindableObject, "bindableObject");

                m_BindableObject = bindableObject;
            }

            /// <summary>
            /// Gets the <see cref="CSW.Framework.Common.Binding.ComplexProperty"/> associated with the specified property name.
            /// </summary>
            /// <value>
            /// The <see cref="CSW.Framework.Common.Binding.ComplexProperty"/> associated with the specified property name.
            /// </value>
            public ComplexProperty this[string propertyName]
            {
                get
                {
                    ComplexProperty value;
                    if (m_BindableObject.m_ComplexPropertyDictionary.TryGetValue(propertyName, out value))
                        return value;
                    else
                        throw new Exception(string.Format("Property '{0}' does not exist in '{1}'.", propertyName, m_BindableObject.GetType()));
                }
            }

            /// <summary>
            /// Returns an enumerator that iterates through a collection.
            /// </summary>
            /// <returns>
            /// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
            /// </returns>
            public IEnumerator<ComplexProperty> GetEnumerator()
            {
                foreach (ComplexProperty item in m_BindableObject.m_ComplexPropertyDictionary.Values)
                    yield return item;
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
        }
    }
}
